Repository navigation
feat(composer): lock the composer and show the prompt as Sending in the feed - #1183
Merged
Merged
Conversation
Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Optimistic prompt ownership was text presence alone, so any earlier committed user row with the same text owned a new optimistic row. The second "continue" of a session was rejected the moment it was minted and the prompt appeared nowhere until the transcript caught up. Once the composer clears at Enter, that gap reads as a lost prompt. A committed row can only be the twin of a prompt submitted at or before it, so ownership now also requires the twin to be no older than the submit, within a 2 s clock tolerance. Rows without timestamps keep the old presence rule. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he feed Enter used to leave the prompt in an editable composer until the provider accepted it, which for Claude is the JSONL acknowledgement and can take seconds. Claude also painted no user row until the transcript caught up. The submit looked ignored and invited a second Enter. Now Enter clears the draft (text and images) and locks the composer read-only while promptDelivery is sending. The prompt appears in the feed at once, dimmed with a "Sending…" caption. Echo providers dim their existing optimistic row, now keyed by submission id. Claude gets a pending-only row that lives exactly as long as the send, so it can never double-render against the committed row or Claude's queue strip. A failed send puts the text and images back into the composer. Escape and Ctrl+C still reach the agent while locked. The optimistic uuid prefix moves into one shared module instead of three literals. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The identity test asserted that two submits in the same millisecond got the same time-based optimistic uuid. That was a documented limit of the diagnostics-only Stage 0. The uuid is now keyed by submission, so the test pins the distinct rows instead. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clearing the draft at Enter and restoring it on failure lost prompts in two cases found in review. A renderer reload mid-send flushed the emptied draft through autosave, which persists only draftInput. A session replacement mid-send transferred an empty draft and restored the failed prompt into the retired session. The draft now stays in the store for the whole send. Only the composer view is emptied while locked, and acceptance strips the sent prompt while keeping text that dictation or templates added during the send. Submit also refuses while sending, because the per-mount in-flight ref does not survive a remount and the hidden draft would otherwise look submittable to the global Enter target. The lock now also swallows Escape and Ctrl+C. Main refuses raw writes for the whole main-owned delivery, so those keys never reached the agent and only produced a false "draft preserved" toast. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude's acceptance is main seeing the prompt's JSONL line, but that line waits in the JSONL coalescer for the next setImmediate, while the invoke reply resumes in a microtask and overtakes it. The renderer removes its pending Sending row when the reply lands, so the prompt blinked out of the feed until the batch arrived. Flushing the session's buffer before replying keeps the committed row ahead of the acceptance on the same channel. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude commits a pasted prompt inside its <pasted_content> envelope while the pending row carries the typed text, so the two never matched and both painted until the send settled. The committed user row's ownership key is now the envelope's inner text, using the same strict whole-string unwrap main uses to recognise the accepted prompt. The test drives the recorded pasted-prompt row. Also moves the last copied optimistic prefix literal onto the shared constant. Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1181 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pressing Enter left the prompt in an editable composer until the provider accepted it. For Claude, acceptance is the JSONL acknowledgement, which can take seconds, and Claude painted no user row until the transcript caught up. The submit looked ignored, and the editable draft invited a second Enter or edits to a prompt already on its way.
Behavior
promptDelivery.kind === 'sending'the composer shows empty and is read-only. The key handler ignores every key, including/, Tab, arrows, Enter, Escape and Ctrl+C. Paste, type-to-focus and paste-to-focus are off.Sending…caption.uncertainbanner still blocks a plain resend.Design notes
draftInput, and a session replacement transfers only current draft fields. On acceptance,draftAfterAcceptancestrips the sent prompt and keeps text that dictation, templates or reply-to-selection added at either end.submitCurrentDraftalso refuses whilesending, because the per-mount in-flight ref does not survive a remount.session:deliver-promptcallsflushJsonl(sessionId)before replying. Otherwise Claude's acceptance reply overtakes the coalesced JSONL batch (microtask vssetImmediate), and the pending row is removed before the committed row exists.pendingEntryUuidand dims the matchingentryitem. Ledger rows, ordering and ownership are untouched, so the D11 identity cache holds.addOptimisticCodexUserEntry. That action's mid-turn branch writesqueuedMessages, which for Claude belongs to the queue-operation reducer.continuewas owned by the first one and vanished at once.<pasted_content>envelope, using the same strict unwrap main uses for acceptance.session-runtime/optimisticPrompt.ts, and every copied literal is gone.Review (one round: Claude + Codex reviewers)
preventDefault-ed, so app shortcuts still work)Tests
useComposerKeybinds.pendingPrompt.renderer.test.tsx. Real workspace controller, real submit hook and real ledger → Feed, with delivery held open. It covers:ownership.optimisticTwin.test.ts:promptDeliveryDraft.test.ts: acceptance stripping.streamingSubmissionIdentity: now pins distinct uuids for same-millisecond submits. It used to pin the collision.npx tsc -bis clean.npm teston Node 24 (before the review fixes): 6479 passed. The 4 failures were 5 s timeouts in untouched files (lazy-prose, settings hydration ×2, ProviderEnablementRow) and fail locally onmaintoo. Affected areas were re-run after the fixes.Known limitations
SendingWorkIndicator chip still shows beside the row caption. It is driven by stream phase and carries the elapsed clock.Fixes #1181
🤖 Generated with Claude Code